#include <string>
#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>

#include <GL/glew.h>
#include <GL/wglew.h>

#include "Shader.hpp"
#include "Renderer.hpp"

bool CShader::ShaderRead(std::string& shaderFile, char** shaderSource)
{
	std::fstream shaderStream;
	shaderStream.open(shaderFile, std::ios::in|std::ios::binary|std::ios::ate);
	if(!shaderStream.good())
	{
		return false;
	}

	std::streamoff shaderSrcLen = shaderStream.tellg();
	*shaderSource = new char[shaderSrcLen+1];
	shaderStream.seekg(0, std::ios::beg);
	shaderStream.read(*shaderSource, shaderSrcLen);

	if(!shaderStream.good())
	{
		delete [] *shaderSource;
		return false;
	}

	shaderStream.close();

	(*shaderSource)[shaderSrcLen] = '\0';

	return true;
}

Shader* CShader::ShaderCompile(GLenum shaderType, std::string& shaderFile, const char* shaderSource)
{
	if(shaderSource == nullptr)
		return nullptr;

	::glProgramParameteri(shaderType, GL_PROGRAM_SEPARABLE, GL_TRUE);
	GLuint programId = ::glCreateShaderProgramv(shaderType, 1, &shaderSource);
	
	delete[] shaderSource;

	if(programId == 0)
		throw("Unable to create program.");

	GetInfoLog(shaderFile.c_str(), programId, GL_LINK_STATUS);

	Shader* shader = new Shader;
	shader->shaderId = programId;
	shader->shaderType = shaderType;
	shader->file = shaderFile;

	return shader;
}

bool CShader::GetInfoLog(const std::string& resource, GLuint index, GLenum type)
{
	GLint res = GL_FALSE;
	GLint logLen = 0;

	bool isPipeline(::glIsProgramPipeline(index) == GL_TRUE);

	switch(type)
	{
	case GL_COMPILE_STATUS:
	case GL_LINK_STATUS:
	case GL_VALIDATE_STATUS:
		if(isPipeline)
		{
			::glGetProgramPipelineiv(index, type, &res);
			::glGetProgramPipelineiv(index, GL_INFO_LOG_LENGTH, &logLen);
		}
		else
		{	
			::glGetProgramiv(index, type, &res);
			::glGetProgramiv(index, GL_INFO_LOG_LENGTH, &logLen);
		}

		if(logLen)
		{
			std::vector<char> log(logLen);

			if(isPipeline)
				::glGetProgramPipelineInfoLog(index, logLen, nullptr, log.data());
			else
				::glGetProgramInfoLog(index, logLen, nullptr, log.data());

			std::cout << resource.c_str() << ":" << std::endl << log.data() << std::endl;
		}
		else
			std::cout << "Unable to obtain program info log for resource " << resource.c_str() << std::endl;

		break;

	default:
		std::cout << "Invalid info log type `" << type << "` specified" << std::endl;
		break;
	}

	return res == GL_TRUE;
}

// Fixme after target compilers implement variadic templates properly
bool CShader::CreateUniforms(std::vector<std::string> uniforms)
{
	return true;
}

// Refactor for per shader uniforms to avoid needlessly wasting resources
ShaderProgram* CShader::CreateProgram(std::vector<std::string>&& uniforms, std::string&& vertexFile, std::string&& fragFile, std::string&& geomFile, std::string&& tessControlFile, std::string&& tessEvalFile)
{
	ShaderProgram* program = new ShaderProgram;

	Shader* shader = nullptr;
	char* buf = nullptr;

	::glGenProgramPipelines(1, &program->pipeline);
	::glBindProgramPipeline(program->pipeline);

	bool success = true;

	if(vertexFile.size())
	{
		if(!ShaderRead(vertexFile, &buf) || (shader = ShaderCompile(GL_VERTEX_SHADER, vertexFile, buf)) == nullptr)
		{
			delete program;
			return nullptr;
		}

		::glUseProgramStages(program->pipeline, GL_VERTEX_SHADER_BIT, shader->shaderId);
	}
	
	if(!CheckForErrors())
		return nullptr;

	program->shaders.push_back(shader);

	shader = nullptr;
	if(fragFile.size())
	{
		if(!ShaderRead(fragFile, &buf) || (shader = ShaderCompile(GL_FRAGMENT_SHADER, fragFile, buf)) == nullptr)
		{
			delete program;
			return nullptr;
		}

		::glUseProgramStages(program->pipeline, GL_FRAGMENT_SHADER_BIT, shader->shaderId);
	}

	if(!CheckForErrors())
		return nullptr;

	program->shaders.push_back(shader);

	shader = nullptr;
	if(geomFile.size())
	{
		if(!ShaderRead(geomFile, &buf) || (shader = ShaderCompile(GL_GEOMETRY_SHADER, geomFile, buf)) == nullptr)
		{
			delete program;
			return nullptr;
		}

		::glUseProgramStages(program->pipeline, GL_GEOMETRY_SHADER_BIT, shader->shaderId);
	}
	
	if(!CheckForErrors())
		return nullptr;

	program->shaders.push_back(shader);

	shader = nullptr;
	if(tessControlFile.size())
	{
		if(!ShaderRead(tessControlFile, &buf) || (shader = ShaderCompile(GL_TESS_CONTROL_SHADER, tessControlFile, buf)) == nullptr)
		{
			delete program;
			return nullptr;
		}

		::glUseProgramStages(program->pipeline, GL_TESS_CONTROL_SHADER_BIT, shader->shaderId);
	}

	if(!CheckForErrors())
		return nullptr;

	program->shaders.push_back(shader);

	shader = nullptr;
	if(tessEvalFile.size())
	{
		if(!ShaderRead(tessEvalFile, &buf) || (shader = ShaderCompile(GL_TESS_EVALUATION_SHADER, tessEvalFile, buf)) == nullptr)
		{
			delete program;
			return nullptr;
		}

		::glUseProgramStages(program->pipeline, GL_TESS_EVALUATION_SHADER_BIT, shader->shaderId);
	}

	if(!CheckForErrors())
		return nullptr;

	program->shaders.push_back(shader);

	GLint uniform;
	for(auto it(program->shaders.begin()), end(program->shaders.end()); it != end; ++it)
	{
		if(*it)
		{
			std::for_each(uniforms.begin(), uniforms.end(), [&](std::string str)
			{
				uniform = ::glGetUniformLocation((*it)->shaderId, str.c_str());
				(*it)->uniforms.push_back(uniform);
			});
		}
	}

	if(!CheckForErrors())
		return nullptr;

	::glValidateProgramPipeline(program->pipeline);

	::glBindProgramPipeline(0);

	GetInfoLog(std::string("Pipeline"), program->pipeline, GL_VALIDATE_STATUS);

	if(!CheckForErrors())
		return nullptr;

	return program;
}
